package com.hb.config.shiro;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hb.commons.MsgEnum;
import com.hb.commons.StringRedisService;
import com.hb.demo.entity.AdminUser;
import com.hb.demo.entity.LoginModel;
import com.hb.demo.service.AdminUserService;
import com.hb.util.CredentialUtil;
import com.hb.util.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Description: 用户登录鉴权和获取用户授权
 * @Author: Scott
 * @Date: 2019-4-23 8:13
 * @Version: 1.1
 */
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {
    @Lazy
    @Resource
    private StringRedisService redisService;
    @Lazy
    @Resource
    private AdminUserService adminUserService;

    /**
     * 必须重写此方法，不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
     * 触发检测用户权限时才会调用此方法，例如checkRole,checkPermission
     *
     * @param principals 身份信息
     *
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 添加角色和权限代码
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 此处仅为示例, 真实权限应从数据库获取
        // 授予角色
        List<String> rolePermissions = CollUtil.newArrayList("admin", "manager");
        simpleAuthorizationInfo.addRoles(rolePermissions);

        // 授予权限
        List<String> funcPermissions = CollUtil.newArrayList("user:add", "user:delete", "user:update");
        simpleAuthorizationInfo.addStringPermissions(funcPermissions);

        return simpleAuthorizationInfo;
    }

    /**
     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
     * 也就是说验证用户输入的账号和密码是否正确，错误抛出异常
     *
     * @param auth 用户登录的账号密码信息
     *
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     *
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.debug("Shiro身份认证(登录) - [doGetAuthenticationInfo]");

        JwtToken token = (JwtToken) auth;

        String username = token.getUsername() != null ? token.getUsername() : JwtUtils.getUsername(token.getToken());
        AdminUser user = adminUserService.getByUsername(username);
        if (user == null) {
            throw new UnknownAccountException(MsgEnum.USERNAME_NOT_FOUND.msg());
        }

        /* 判断用户状态等操作, 用户被禁用, 用户列入黑名单等操作 */
        Integer setups = user.getSetups();
        if (setups == null || setups != 1) {
            throw new DisabledAccountException(MsgEnum.USER_DISABLE.msg());
        }

        // 完善信息 start
        token.setId(user.getId());
        if (token.getUsername() == null) {
            token.setUsername(user.getUname());
        }
        // 完善信息 end

        // 返回给前端的token字符串
        if (StrUtil.isEmpty(token.getToken())) { // 为空时表示第一次登录, 否则为接口调用时携带的token
            String tokenSign = JwtUtils.geneJsonWebToken(user);
            token.setFullName(user.getRealName()); // 用户姓名
            token.setToken(tokenSign);
            String frontPassword = token.getPassword(); // 前端登录时传递的用户密码
            token.setPassword(user.getPword()); // 数据库中保存的用户名密码

            // 密码错误时会在这里抛出异常
            return new SimpleAuthenticationInfo(token, CredentialUtil.md5(frontPassword), getName());
        }

        // 刷新redis中的token
        checkUserTokenIsEffect(token.getToken(), user);

        // 密码错误时会在这里抛出异常
        return new SimpleAuthenticationInfo(token, user.getPword(), getName());
    }

    /**
     * 校验token的有效性
     * 此方法在第一次登录时不调用
     * 但是每次请求接口时都会调用该方法进行验证
     *
     * @param token 请求接口时携带的token
     * @param user  数据库中保存的user对象
     */
    public void checkUserTokenIsEffect(String token, AdminUser user) throws AuthenticationException {
        if (user == null) {
            throw new AuthenticationException(MsgEnum.USERNAME_NOT_FOUND.msg());
        }
        // 解密获得username，用于和数据库进行对比
        String username = JwtUtils.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }

        /* 将token中的用户名, 密码等属性与user对象进行比对, 分辨用户修改密码或其他操作 */
        if (!user.getPword().equals(JwtUtils.getPassword(token))) {
            throw new AuthenticationException("密码已经更改, 请重新登录!");
        }
        /* ...... */
        /* 严谨的逻辑一顿操作 */

        log.debug("校验token是否有效[{}]", "com.hb.config.shiro.ShiroRealm.checkUserTokenIsEffect");

        // 校验token是否超时失效
        if (!jwtTokenRefresh(token)) {
            throw new AuthenticationException(MsgEnum.LOGIN_EXPIRE.msg());
        }
    }

    /**
     * JWTToken刷新生命周期 （实现： 用户在线操作不掉线功能）
     *
     * @param token 请求接口时携带的token
     *
     * @return
     */
    public boolean jwtTokenRefresh(String token) {
        // token中的用户名
        String username = JwtUtils.getUsername(token);
        // 获取redis中的token
        String loginModelJSON = redisService.get(JwtUtils.PREFIX_USER_TOKEN + username);
        // redis中没有该用户的token表示空闲时间太长需要重新登录, 这时直接返回false
        if (StrUtil.isNotEmpty(loginModelJSON)) {
            // 转换为登录对象模型
            LoginModel model = JSONUtil.toBean(loginModelJSON, LoginModel.class);
            if (model == null) { // 转不了的就去他奶奶个腿
                return false;
            }

            String redisToken = model.getToken(); // redis中保存的token

            if (!token.equals(redisToken)) { // token不同可能是同一个用户在别处登录
                return false;
            }

            // 校验token时效性
            if (JwtUtils.verify(model.getToken())) {
                // 设置超时时间
                redisService.expire(JwtUtils.PREFIX_USER_TOKEN + username, JwtUtils.EXPIRE);
                // log.info("用户在线操作，更新token保证不掉线[{}]", "com.hb.home.config.shiro.ShiroRealm.jwtTokenRefresh");
                return true;
            }
        }
        return false;
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

}
